﻿#region Copyright Notices
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Library General Public
// License as published by the Free Software Foundation; either
// version 2 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Library General Public License for more details.
// 
// You should have received a copy of the GNU Library General Public
// License along with this library; if not, write to the Free
// Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#endregion

using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
using System.Reflection;
using System.Globalization;

namespace Yarr
{
    internal static class MethodCache
    {
        #region Private members
        private const int CAPACITY = 100;

        private static Dictionary<String, MethodInfo> instanceMethodTable;
        private static Dictionary<String, MethodInfo> staticMethodTable;

        private static Dictionary<String, PropertyInfo> instancePropertyTable;
        private static Dictionary<String, PropertyInfo> staticPropertyTable;

        private static Dictionary<String, FieldInfo> instanceFieldTable;
        private static Dictionary<String, FieldInfo> staticFieldTable;

        private static Dictionary<String, MethodInfo> extensionMethodTable;
        private static HashSet<Type> extensionTypeTable;
        #endregion


        #region Properites
        internal static HashSet<Type> ImplementingTypes
        {
            get { return extensionTypeTable; }
        }

        internal static ICollection<String> CachedExtensionMethods
        {
            get { return extensionMethodTable.Keys; }
        }
        #endregion


        #region Constructors
        static MethodCache()
		{
            instanceMethodTable = new Dictionary<String, MethodInfo>(CAPACITY);
            staticMethodTable = new Dictionary<String, MethodInfo>(CAPACITY);

            instancePropertyTable = new Dictionary<String, PropertyInfo>(CAPACITY);
            staticPropertyTable = new Dictionary<String, PropertyInfo>(CAPACITY);

            instanceFieldTable = new Dictionary<String, FieldInfo>(CAPACITY);
            staticFieldTable = new Dictionary<String, FieldInfo>(CAPACITY);

            extensionMethodTable = new Dictionary<String, MethodInfo>(CAPACITY);
            extensionTypeTable = new HashSet<Type>();

            InitializeExtensions();
        }
        #endregion


        #region Public methods
        public static FieldInfo FindField(string fieldName, Type type, BindingFlags bindingFlags)
        {
            Dictionary<String, FieldInfo> fieldTable =
                (bindingFlags & BindingFlags.Static) == BindingFlags.Static ?
                        staticFieldTable : instanceFieldTable;

            string fieldKey = String.Format(CultureInfo.InvariantCulture, "{0}.{1}", type.Name, fieldName);

            lock (fieldTable)
            {
                if (fieldTable.ContainsKey(fieldKey))
                    return fieldTable[fieldKey];
            }

            FieldInfo f = type.GetField(fieldName, bindingFlags);

            if (f != null)
            {
                lock (fieldTable)
                {
                    fieldTable[fieldKey] = f;
                }
            }

            return f;
        }


        public static PropertyInfo FindProperty(string propName, Type type, Type[] parameterTypes,
                                            BindingFlags bindingFlags)
        {
            Dictionary<String, PropertyInfo> propertyTable =
                (bindingFlags & BindingFlags.Static) == BindingFlags.Static ?
                        staticPropertyTable : instancePropertyTable;

            string propKey = ConstructPropertyTableKey(propName, type, parameterTypes, bindingFlags);

            lock (propertyTable)
            {
                if (propertyTable.ContainsKey(propKey))
                    return propertyTable[propKey];
            }

            PropertyInfo p = type.GetProperty(propName, bindingFlags, null, null, parameterTypes, null);

            if (p != null)
            {
                lock (propertyTable)
                {
                    propertyTable[propKey] = p;
                }
            }

            return p;
        }


        public static MethodInfo FindMethod(string methodName, Type type, Type[] parameterTypes,
                                            Object[] arguments, BindingFlags bindingFlags)
        {
            Dictionary<String, MethodInfo> methodTable = 
                (bindingFlags & BindingFlags.Static) == BindingFlags.Static ? 
                        staticMethodTable : instanceMethodTable;

            string methodKey = ConstructMethodTableKey(methodName, type, parameterTypes);

            lock (methodTable)
            {
                if (methodTable.ContainsKey(methodKey))
                    return methodTable[methodKey];
            }

            MethodInfo m = type.GetMethod(methodName, bindingFlags | BindingFlags.InvokeMethod,
                                          null, parameterTypes, null);

            // If arguments are provided, look for a generic method

            if (m == null && arguments != null)
            {
                MethodInfo[] methods = type.GetMethods(bindingFlags);

                // Find a generic method that takes the same number of parameters as we're trying to pass
                foreach (MethodInfo mi in methods)
                {
                    ParameterInfo[] piList = mi.GetParameters();

                    if (String.Compare(methodName, mi.Name, true) == 0 &&
                        piList.Length == parameterTypes.Length &&
                        mi.IsGenericMethod)
                    {
                        // The number of generic parameters we'll need to resolve
                        Type[] genParams = new Type[mi.GetGenericArguments().Length];

                        // Get the candidate types for the generic parameters ...
                        HashSet<Type> candidates = new HashSet<Type>();
                        foreach (Type t in parameterTypes)
                        {
                            candidates.Add(t);
                            if (t.HasElementType)
                                candidates.Add(t.GetElementType());

                            if (t.IsGenericType)
                                foreach (Type t2 in t.GetGenericArguments())
                                    candidates.Add(t2);
                        }

                        // ... put the candidates into an array
                        Type[] typeCandidates = new Type[candidates.Count];
                        candidates.CopyTo(typeCandidates);

                        // We'll compute index combinations here
                        int[] combos = new int[genParams.Length];

                        bool paramsMatch = false;

                        do
                        {
                            for (int ix = 0; ix < genParams.Length; ++ix)
                                genParams[ix] = typeCandidates[combos[ix]];

                            m = mi.MakeGenericMethod(genParams);
                            if (m != null)
                            {
                                // If parameters are provided, use them to
                                // Verify that we've got the correct method
                                paramsMatch = true;
                                piList = m.GetParameters();

                                foreach (ParameterInfo pi in piList)
                                {
                                    if (!Primitives.IsTypeOf(pi.ParameterType, arguments[pi.Position]))
                                    {
                                        paramsMatch = false;
                                        break;
                                    }
                                }
                            }
                        }
                        while (!paramsMatch && NextCombination(combos, typeCandidates.Length));

                        if (paramsMatch)
                            break;
                        else
                            m = null;
                    }
                }
            }

            if (m != null)
            {
                lock (methodTable)
                {
                    methodTable[methodKey] = m;
                }
            }

            return m;
        }

        private static bool NextCombination(int[] combos, int setSize)
        {
            if (combos == null)
                return false;

            int ix = 0;
            while (ix < combos.Length  &&  ++combos[ix] == setSize)
                combos[ix++] = 0;

            return ix < combos.Length;
        }


        public static MethodInfo FindExtensionMethod(string methodName, ref Object instance, Type type, 
                                                     Type[] parameterTypes, ref Object[] arguments, 
                                                     BindingFlags bindingFlags)
        {
            lock (extensionTypeTable)
            {
                if (extensionTypeTable.Count == 0)
                    return null;
            }

            string methodKey = ConstructMethodTableKey(methodName, type, parameterTypes);
            MethodInfo foundMethod = null;

            lock (extensionMethodTable)
            {
                if (extensionMethodTable.ContainsKey(methodKey))
                    foundMethod = extensionMethodTable[methodKey];
            }

            Object[] extensionArguments =
                arguments == null ? new Object[1] : new Object[arguments.Length + 1];
            extensionArguments[0] = instance;
            if (arguments != null)
                for (int ix = 0; ix < arguments.Length; ++ix)
                    extensionArguments[ix + 1] = arguments[ix];

            if (foundMethod == null)
            {
                BindingFlags extensionBindings = (bindingFlags & ~BindingFlags.Instance)
                                                    | BindingFlags.Static | BindingFlags.FlattenHierarchy;
                Type[] extensionParameterTypes =
                    parameterTypes == null ? new Type[1] : new Type[parameterTypes.Length + 1];
                extensionParameterTypes[0] = type;
                if (parameterTypes != null)
                    for (int ix = 0; ix < parameterTypes.Length; ++ix)
                        extensionParameterTypes[ix + 1] = parameterTypes[ix];

                lock (extensionTypeTable)
                {
                    foreach (Type t in extensionTypeTable)
                    {
                        foundMethod = FindMethod(methodName, t, extensionParameterTypes,
                                                    extensionArguments, extensionBindings);

                        // Verify that this is actually an extension method
                        if (foundMethod != null)
                        {
                            if (IsExtensionMethod(foundMethod))
                                break;
                            else
                                foundMethod = null;
                        }
                    }
                }
            }

            if (foundMethod != null)
            {
                lock (extensionMethodTable)
                {
                    extensionMethodTable[methodKey] = foundMethod;
                }

                arguments = extensionArguments;
                instance = null;
            }

            return foundMethod;
        }


        public static void RegisterExtensionType(Type implementingType)
        {
            lock (extensionTypeTable)
            {
                extensionTypeTable.Add(implementingType);
            }
        }

        public static void RegisterExtensionAssembly(Assembly assembly)
        {
            BindingFlags flags = BindingFlags.Public |
                                BindingFlags.Static |
                                BindingFlags.FlattenHierarchy;
            
            Type[] types = assembly.GetTypes();

            if (types != null)
            {
                foreach (Type t in types)
                {
                    if (t.IsPublic && t.IsClass && !t.IsGenericTypeDefinition)
                    {
                        MethodInfo[] methods = t.GetMethods(flags);
                        if (methods != null)
                        {
                            foreach (MethodInfo m in methods)
                            {
                                if (IsExtensionMethod(m))
                                {
                                    RegisterExtensionType(t);
                                    break;
                                }
                            }
                        }
                    }
                }
            }
        }


        internal static bool IsExtensionMethod(MethodInfo methodInfo)
        {
            object[] attrs =
                methodInfo.GetCustomAttributes(
                    typeof(System.Runtime.CompilerServices.ExtensionAttribute), true);

            return (attrs != null && attrs.Length > 0);
        }
        #endregion


        #region Helper methods
        private static string ConstructPropertyTableKey(string propName, Type type, 
                                                        Type[] parameterTypes, BindingFlags bindingFlags)
        {
            StringBuilder sb = new StringBuilder(String.Format(CultureInfo.InvariantCulture,
                            "{2}:{0}.{1}", type.Name, propName,
                            (bindingFlags & BindingFlags.SetProperty) == BindingFlags.SetProperty ?
                                   "Set:" : "Get:"));

            if (parameterTypes != null && parameterTypes.Length > 0)
            {
                sb.Append('[');
                AppendParameterList(sb, parameterTypes);
                sb.Append(']');
            }

            return sb.ToString();
        }

        private static string ConstructMethodTableKey(string methodName, Type type, Type[] parameterTypes)
        {
            StringBuilder sb = new StringBuilder(String.Format(CultureInfo.InvariantCulture, 
                                                                "{0}.{1}(", type.Name, methodName));
            AppendParameterList(sb, parameterTypes);
            sb.Append(')');

            return sb.ToString();
        }

        private static void AppendParameterList(StringBuilder sb, Type[] parameterTypes)
        {
            for (int ix = 0; ix < parameterTypes.Length; ++ix)
                sb.AppendFormat("{0}{1}", parameterTypes[ix].ToString(),
                                          (ix == parameterTypes.Length - 1) ? "" : ",");
        }

        private static void InitializeExtensions()
        {

            foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
                RegisterExtensionAssembly(assembly);
        }
        #endregion
    }
}
